/**
 * Type constraint that requires that the type has a certain attribute
 */
package com.starview.platform.data.type;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.starview.star.compiler.transform.OverLoader;
import com.starview.star.compiler.transform.PrimitiveOverloader;
import com.starview.star.compiler.type.Dictionary;
import com.starview.star.compiler.type.TypeContracts;
import com.starview.star.compiler.util.FixedList;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.StringUtils;

@SuppressWarnings("serial")
public class ContractConstraint implements ITypeConstraint
{
  private final TypeExp contract;

  public ContractConstraint(TypeExp contract)
  {
    this.contract = contract;
    assert contract != null;
  }

  public ContractConstraint(String contract, List<IType> argTypes)
  {
    this.contract = (TypeExp) TypeUtils.typeExp(contract, argTypes);
  }

  public ContractConstraint(String contract, IType... argTypes)
  {
    this.contract = (TypeExp) TypeUtils.typeExp(contract, argTypes);
  }

  public TypeExp getContract()
  {
    return contract;
  }

  public String getContractName()
  {
    return contract.typeLabel();
  }

  public IType[] getContractTypes()
  {
    return contract.getTypeArgs();
  }

  @Override
  public Collection<TypeVar> affectedVars()
  {
    List<TypeVar> vars = new ArrayList<>();
    for (IType t : contract.getTypeArgs()) {
      t = TypeUtils.deRef(t);
      if (t instanceof TypeVar)
        vars.add((TypeVar) t);
    }
    return vars;
  }

  @Override
  public void accept(ITypeVisitor visitor)
  {
    contract.accept(visitor);
  }

  @Override
  public <T, C> C transform(TypeTransformer<T, C> trans)
  {
    return trans.transformContractConstraint(this);
  }

  @Override
  public boolean sameConstraint(ITypeConstraint other, Location loc, Dictionary dict) throws TypeConstraintException
  {
    if (other instanceof ContractConstraint) {
      try {
        ContractConstraint otherCon = (ContractConstraint) other;
        Subsume.same(otherCon.contract, contract, loc, dict);
        return true;
      } catch (TypeConstraintException e) {
        return false;
      }
    }
    return false;
  }

  @Override
  public boolean equals(Object obj)
  {
    return obj instanceof ContractConstraint && ((ContractConstraint) obj).getContract().equals(getContract());
  }

  @Override
  public int hashCode()
  {
    if (getContract() == null)
      return tempHashCode;
    int hashCode = getContract().hashCode();
    assert (tempHashCode == 0 || tempHashCode == hashCode);
    return hashCode;
  }

  @Override
  public void checkBinding(IType candidate, Location loc, Dictionary dict) throws TypeConstraintException
  {
    candidate = TypeUtils.deRef(candidate);

    IType fallbackType = dict.getVarType(TypeContracts.contractFallbackName(contract));

    if (TypeUtils.isFunType(candidate) && fallbackType == null)
      throw new TypeConstraintException("contract cannot be implemented for function types");
    else if (TypeUtils.isRawType(candidate)
        && /* fallbackType == null && */!PrimitiveOverloader.hasPrimitiveImplementation(getContract()))
      throw new TypeConstraintException(StringUtils.msg("contract ", getContract(),
          " is not implemented for raw type ", candidate));
    else {
      TypeExp contract = getContract();
      if (TypeUtils.isGroundSurface(contract)) {
        String instanceName = OverLoader.instanceFunName(TypeContracts.contractImplType(contract));
        IType instanceType = dict.getVarType(instanceName);
        if (instanceType == null) {
          if (fallbackType == null)
            throw new TypeConstraintException(FixedList.create(contract.toString(), " not known to be implemented"));
          else {
            if (TypeUtils.isOverloadedType(fallbackType)) {
              IType instType = TypeUtils.refreshOverloaded(fallbackType);
              Subsume.subsume(contract, TypeUtils.getOverloadedType(instType), loc, dict);
            }
          }
        } else {
          if (TypeUtils.isOverloadedType(instanceType)) {
            IType instType = TypeUtils.refreshOverloaded(instanceType);
            Subsume.subsume(contract, TypeUtils.getOverloadedType(instType), loc, dict, false);
          }
        }
      }
    }
  }

  @Override
  public void showConstraint(DisplayType disp)
  {
    IType[] contractArgs = contract.getTypeArgs();

    disp.getDisp().appendId(contract.typeLabel());
    disp.getDisp().appendWord(AbstractType.OVER);
    disp.typeArgs(contractArgs);
  }

  @Override
  public String toString()
  {
    PrettyPrintDisplay disp = new PrettyPrintDisplay();
    DisplayType.displayContract(disp, contract);

    return disp.toString();
  }

  int tempHashCode = 0;

  private void writeObject(ObjectOutputStream os) throws IOException
  {
    os.writeInt(hashCode());
    os.defaultWriteObject();
  }

  private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException
  {
    tempHashCode = is.readInt();
    is.defaultReadObject();
  }
}